/**
 * This class lets you encode animated GIF files
 * Base class :
 * http://www.java2s.com/Code/Java/2D-Graphics-GUI/AnimatedGifEncoder.htm
 * @author Kevin Weiner (original Java version - kweiner@fmsware.com)
 * @author Thibault Imbert (AS3 version - bytearray.org)
 * @version 0.1 AS3 implementation
 */

package com.siao.component.gif.encoder
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.utils.ByteArray;
	
	public class GIFEncoder
	{
		private var width:int // image size
		
		private var height:int;
		
		private var transparent:* = null; // transparent color if given
		
		private var transIndex:int; // transparent index in color table
		
		private var repeat:int = -1; // no repeat
		
		private var delay:int = 0; // frame delay (hundredths)
		
		private var started:Boolean = false; // ready to output frames
		
		private var out:ByteArray;
		
		private var image:Bitmap; // current frame
		
		private var pixels:ByteArray; // BGR byte array from frame
		
		private var indexedPixels:ByteArray // converted frame indexed to palette
		
		private var colorDepth:int; // number of bit planes
		
		private var colorTab:ByteArray; // RGB palette
		
		private var usedEntry:Array = new Array; // active palette entries
		
		private var palSize:int = 7; // color table size (bits-1)
		
		private var dispose:int = -1; // disposal code (-1 = use default)
		
		private var closeStream:Boolean = false; // close stream when finished
		
		private var firstFrame:Boolean = true;
		
		private var sizeSet:Boolean = false; // if false, get size from first frame
		
		private var sample:int = 10; // default sample interval for quantizer
		
		/**
		 * Sets the delay time between each frame, or changes it for subsequent
		 * frames
		 * (applies to last frame added)
		 * int delay time in milliseconds
		 * @param ms
		 */
		
		public function setDelay(ms:int):void
		{
			
			delay = Math.round(ms / 10);
		
		}
		
		/**
		 * Sets the GIF frame disposal code for the last added frame and any
		 *
		 * subsequent frames. Default is 0 if no transparent color has been set,
		 * otherwise 2.
		 * @param code
		 * int disposal code.
		 */
		
		public function setDispose(code:int):void
		{
			
			if (code >= 0)
				dispose = code;
		
		}
		
		/**
		 * Sets the number of times the set of GIF frames should be played.
		 * Default is
		 * 1; 0 means play indefinitely. Must be invoked before the first image is
		 * added.
		 *
		 * @param iter
		 * int number of iterations.
		 * @return
		 */
		
		public function setRepeat(iter:int):void
		{
			
			if (iter >= 0)
				repeat = iter;
		
		}
		
		/**
		 * Sets the transparent color for the last added frame and any subsequent
		 * frames. Since all colors are subject to modification in the quantization
		 * process, the color in the final palette for each frame closest to the
		 * given
		 * color becomes the transparent color for that frame. May be set to null
		 * to
		 * indicate no transparent color.
		 * @param
		 * Color to be treated as transparent on display.
		 */
		
		public function setTransparent(c:Number):void
		{
			
			transparent = c;
		
		}
		
		/**
		 * The addFrame method takes an incoming BitmapData object to create each
		 * frames
		 * @param
		 * BitmapData object to be treated as a GIF's frame
		 */
		
		public function addFrame(im:BitmapData):Boolean
		{
			
			if ((im == null) || !started || out == null)
				
			{
				throw new Error("Please call start method before calling addFrame");
				return false;
			}
			
			var ok:Boolean = true;
			
			try
			{
				
				image = new Bitmap(im);
				
				if (!sizeSet)
					setSize(image.width, image.height);
				getImagePixels(); // convert to correct format if necessary
				analyzePixels(); // build color table & map pixels
				
				if (firstFrame)
				{
					writeLSD(); // logical screen descriptior
					writePalette(); // global color table
					
					if (repeat >= 0)
					{
						// use NS app extension to indicate reps
						writeNetscapeExt();
					}
				}
				
				writeGraphicCtrlExt(); // write graphic control extension
				writeImageDesc(); // image descriptor
				
				if (!firstFrame)
					writePalette(); // local color table
				writePixels(); // encode and write pixel data
				firstFrame = false;
			}
			catch (err:Error)
			{
				ok = false;
			}
			
			return ok;
		
		}
		
		/**
		 * Adds final trailer to the GIF stream, if you don't call the finish
		 * method
		 * the GIF stream will not be valid.
		 */
		
		public function finish():Boolean
		{
			if (!started)
				return false;
			var ok:Boolean = true;
			started = false;
			
			try
			{
				out.writeByte(0x3b); // gif trailer
			}
			catch (err:Error)
			{
				ok = false;
			}
			
			return ok;
		
		}
		
		/**
		 * Resets some members so that a new stream can be started.
		 * This method is actually called by the start method
		 */
		
		private function reset():void
		{
			
			// reset for subsequent use
			transIndex = 0;
			image = null;
			pixels = null;
			indexedPixels = null;
			colorTab = null;
			closeStream = false;
			firstFrame = true;
		
		}
		
		/**
		 * * Sets frame rate in frames per second. Equivalent to
		 * <code>setDelay(1000/fps)</code>.
		 * @param fps
		 * float frame rate (frames per second)
		 */
		
		public function setFrameRate(fps:Number):void
		{
			
			if (fps != 0xf)
				delay = Math.round(100 / fps);
		
		}
		
		/**
		 * Sets quality of color quantization (conversion of images to the maximum
		 * 256
		 * colors allowed by the GIF specification). Lower values (minimum = 1)
		 * produce better colors, but slow processing significantly. 10 is the
		 * default, and produces good color mapping at reasonable speeds. Values
		 * greater than 20 do not yield significant improvements in speed.
		 * @param quality
		 * int greater than 0.
		 * @return
		 */
		
		public function setQuality(quality:int):void
		{
			
			if (quality < 1)
				quality = 1;
			sample = quality;
		
		}
		
		/**
		 * Sets the GIF frame size. The default size is the size of the first frame
		 * added if this method is not invoked.
		 * @param w
		 * int frame width.
		 * @param h
		 * int frame width.
		 */
		
		private function setSize(w:int, h:int):void
		{
			
			if (started && !firstFrame)
				return;
			width = w;
			height = h;
			
			if (width < 1)
				width = 320;
			
			if (height < 1)
				height = 240;
			sizeSet = true
		
		}
		
		/**
		 * Initiates GIF file creation on the given stream.
		 * @param os
		 * OutputStream on which GIF images are written.
		 * @return false if initial write failed.
		 *
		 */
		
		public function start():Boolean
		{
			
			reset();
			var ok:Boolean = true;
			closeStream = false;
			out = new ByteArray;
			
			try
			{
				out.writeUTFBytes("GIF89a"); // header
			}
			catch (err:Error)
			{
				ok = false;
			}
			
			return started = ok;
		
		}
		
		/**
		 * Analyzes image colors and creates color map.
		 */
		
		private function analyzePixels():void
		{
			
			var len:int = pixels.length;
			var nPix:int = len / 3;
			indexedPixels = new ByteArray;
			var nq:NeuQuant = new NeuQuant(pixels, len, sample);
			// initialize quantizer
			colorTab = nq.process(); // create reduced palette
			// map image pixels to new palette
			var k:int = 0;
			
			for (var j:int = 0; j < nPix; j++)
			{
				var index:int = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff);
				usedEntry[index] = true;
				indexedPixels[j] = index;
			}
			pixels = null;
			colorDepth = 8;
			palSize = 7;
			
			// get closest match to transparent color if specified
			if (transparent != null)
			{
				transIndex = findClosest(transparent);
			}
		}
		
		/**
		 * Returns index of palette color closest to c
		 *
		 */
		
		private function findClosest(c:Number):int
		{
			
			if (colorTab == null)
				return -1;
			var r:int = (c & 0xFF0000) >> 16;
			var g:int = (c & 0x00FF00) >> 8;
			var b:int = (c & 0x0000FF);
			var minpos:int = 0;
			var dmin:int = 256 * 256 * 256;
			var len:int = colorTab.length;
			
			for (var i:int = 0; i < len; )
			{
				var dr:int = r - (colorTab[i++] & 0xff);
				var dg:int = g - (colorTab[i++] & 0xff);
				var db:int = b - (colorTab[i] & 0xff);
				var d:int = dr * dr + dg * dg + db * db;
				var index:int = i / 3;
				
				if (usedEntry[index] && (d < dmin))
				{
					dmin = d;
					minpos = index;
				}
				i++;
			}
			return minpos;
		
		}
		
		/**
		 * Extracts image pixels into byte array "pixels
		 */
		
		private function getImagePixels():void
		{
			
			var w:int = image.width;
			var h:int = image.height;
			pixels = new ByteArray;
			
			var count:int = 0;
			
			for (var i:int = 0; i < h; i++)
			{
				
				for (var j:int = 0; j < w; j++)
				{
					
					var pixel:Number = image.bitmapData.getPixel(j, i);
					
					pixels[count] = (pixel & 0xFF0000) >> 16;
					count++;
					pixels[count] = (pixel & 0x00FF00) >> 8;
					count++;
					pixels[count] = (pixel & 0x0000FF);
					count++;
					
				}
				
			}
		
		}
		
		/**
		 * Writes Graphic Control Extension
		 */
		
		private function writeGraphicCtrlExt():void
		{
			
			out.writeByte(0x21); // extension introducer
			out.writeByte(0xf9); // GCE label
			out.writeByte(4); // data block size
			var transp:int
			var disp:int;
			
			if (transparent == null)
			{
				transp = 0;
				disp = 0; // dispose = no action
			}
			else
			{
				transp = 1;
				disp = 2; // force clear if using transparent color
			}
			
			if (dispose >= 0)
			{
				disp = dispose & 7; // user override
			}
			disp <<= 2;
			// packed fields
			out.writeByte(0 | // 1:3 reserved
						  disp | // 4:6 disposal
						  0 | // 7 user input - 0 = none
						  transp); // 8 transparency flag
			
			WriteShort(delay); // delay x 1/100 sec
			out.writeByte(transIndex); // transparent color index
			out.writeByte(0); // block terminator
		
		}
		
		/**
		 * Writes Image Descriptor
		 */
		
		private function writeImageDesc():void
		{
			
			out.writeByte(0x2c); // image separator
			WriteShort(0); // image position x,y = 0,0
			WriteShort(0);
			WriteShort(width); // image size
			WriteShort(height);
			
			// packed fields
			if (firstFrame)
			{
				// no LCT - GCT is used for first (or only) frame
				out.writeByte(0);
			}
			else
			{
				// specify normal LCT
				out.writeByte(0x80 | // 1 local color table 1=yes
							  0 | // 2 interlace - 0=no
							  0 | // 3 sorted - 0=no
							  0 | // 4-5 reserved
							  palSize); // 6-8 size of color table
			}
		}
		
		/**
		 * Writes Logical Screen Descriptor
		 */
		
		private function writeLSD():void
		{
			
			// logical screen size
			WriteShort(width);
			WriteShort(height);
			// packed fields
			out.writeByte((0x80 | // 1 : global color table flag = 1 (gct used)
						  0x70 | // 2-4 : color resolution = 7
						  0x00 | // 5 : gct sort flag = 0
						  palSize)); // 6-8 : gct size
			
			out.writeByte(0); // background color index
			out.writeByte(0); // pixel aspect ratio - assume 1:1
		
		}
		
		/**
		 * Writes Netscape application extension to define repeat count.
		 */
		
		private function writeNetscapeExt():void
		{
			
			out.writeByte(0x21); // extension introducer
			out.writeByte(0xff); // app extension label
			out.writeByte(11); // block size
			out.writeUTFBytes("NETSCAPE" + "2.0"); // app id + auth code
			out.writeByte(3); // sub-block size
			out.writeByte(1); // loop sub-block id
			WriteShort(repeat); // loop count (extra iterations, 0=repeat forever)
			out.writeByte(0); // block terminator
		
		}
		
		/**
		 * Writes color table
		 */
		
		private function writePalette():void
		{
			
			out.writeBytes(colorTab, 0, colorTab.length);
			var n:int = (3 * 256) - colorTab.length;
			
			for (var i:int = 0; i < n; i++)
				out.writeByte(0);
		
		}
		
		private function WriteShort(pValue:int):void
		{
			
			out.writeByte(pValue & 0xFF);
			out.writeByte((pValue >> 8) & 0xFF);
		
		}
		
		/**
		 * Encodes and writes pixel data
		 */
		
		private function writePixels():void
		{
			
			var myencoder:LZWEncoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
			myencoder.encode(out);
		
		}
		
		/**
		 * retrieves the GIF stream
		 */
		public function get stream():ByteArray
		{
			
			return out;
		
		}
	
	}
}
